from html import escape
from html.parser import HTMLParser
from urllib.parse import urlparse

from django import forms

from learning.models import Mistake, NotebookEntry


class MultiFileInput(forms.ClearableFileInput):
    allow_multiple_selected = True


class _NotebookHTMLSanitizer(HTMLParser):
    allowed_tags = {
        "p",
        "div",
        "strong",
        "em",
        "u",
        "s",
        "ul",
        "ol",
        "li",
        "blockquote",
        "br",
        "span",
        "a",
        "h1",
        "h2",
        "h3",
        "pre",
        "code",
    }
    allowed_attrs = {
        "a": {"href", "title"},
        "p": {"class"},
        "div": {"class"},
        "span": {"class"},
        "li": {"class"},
        "h1": {"class"},
        "h2": {"class"},
        "h3": {"class"},
        "pre": {"class", "spellcheck"},
        "code": {"class"},
        "blockquote": {"class"},
    }

    def __init__(self):
        super().__init__()
        self.parts: list[str] = []

    def handle_starttag(self, tag, attrs):
        if tag not in self.allowed_tags:
            return
        attr_str = self._format_attrs(tag, attrs)
        self.parts.append(f"<{tag}{attr_str}>")

    def handle_endtag(self, tag):
        if tag in self.allowed_tags and tag != "br":
            self.parts.append(f"</{tag}>")

    def handle_startendtag(self, tag, attrs):
        if tag not in self.allowed_tags:
            return
        attr_str = self._format_attrs(tag, attrs)
        if tag == "br":
            self.parts.append("<br>")
        else:
            self.parts.append(f"<{tag}{attr_str}></{tag}>")

    def handle_data(self, data):
        self.parts.append(escape(data))

    def handle_entityref(self, name):
        self.parts.append(f"&{name};")

    def handle_charref(self, name):
        self.parts.append(f"&#{name};")

    def get_content(self) -> str:
        return "".join(self.parts)

    def _format_attrs(self, tag, attrs):
        allowed = self.allowed_attrs.get(tag, set())
        cleaned = []
        for name, value in attrs:
            if name not in allowed:
                continue
            value = value.strip()
            if not value:
                continue
            if name == "href":
                parsed = urlparse(value)
                if parsed.scheme and parsed.scheme not in ("http", "https", "mailto"):
                    continue
            if name == "class":
                classes = [cls for cls in value.split() if cls.startswith("ql-")]
                if not classes:
                    continue
                value = " ".join(classes)
            if name == "spellcheck":
                value_lower = value.lower()
                if value_lower not in {"true", "false"}:
                    continue
                value = value_lower
            cleaned.append(f'{name}="{escape(value, quote=True)}"')
        return f" {' '.join(cleaned)}" if cleaned else ""


def sanitize_note_content(value: str) -> str:
    parser = _NotebookHTMLSanitizer()
    parser.feed(value)
    parser.close()
    return parser.get_content()


class MistakeForm(forms.ModelForm):
    class Meta:
        model = Mistake
        fields = [
            "title",
            "question",
            "question_image",
            "solution_attempt",
            "solution_attempt_image",
            "correct_solution",
            "reflection",
        ]
        widgets = {
            "question": forms.Textarea(attrs={"rows": 4}),
            "solution_attempt": forms.Textarea(attrs={"rows": 3}),
            "correct_solution": forms.Textarea(attrs={"rows": 3}),
            "reflection": forms.Textarea(attrs={"rows": 3}),
        }
        labels = {
            "title": "标题",
            "question": "题目描述",
            "question_image": "题目图片",
            "solution_attempt": "我的解题过程",
            "solution_attempt_image": "解题过程图片",
            "correct_solution": "正确解法",
            "reflection": "反思总结",
        }

    def save(self, commit=True, student=None):
        mistake = super().save(commit=False)
        if student is not None:
            mistake.student = student
        if commit:
            mistake.save()
        return mistake


class LLMQuestionForm(forms.Form):
    question = forms.CharField(
        label="问题",
        widget=forms.Textarea(attrs={"rows": 3}),
        max_length=1000,
        required=True,
        error_messages={
            'required': '请输入您的问题',
            'max_length': '问题内容不能超过1000个字符',
        }
    )

    def clean_question(self):
        data = self.cleaned_data.get("question", "").strip()
        if not data:
            raise forms.ValidationError("问题内容不能为空")
        return data


class NotebookEntryForm(forms.ModelForm):
    attachments = forms.FileField(
        label="插入图片",
        required=False,
        widget=MultiFileInput(
            attrs={
                "multiple": True,
                "accept": "image/*",
            }
        ),
        help_text="可一次性上传多张图片，将在文章尾部展示。",
    )

    class Meta:
        model = NotebookEntry
        fields = [
            "title",
            "content",
            "related_mistakes",
        ]
        widgets = {
            "title": forms.TextInput(attrs={"placeholder": "为笔记取一个标题"}),
            "content": forms.HiddenInput(),
            "related_mistakes": forms.SelectMultiple(
                attrs={"class": "notebook-mistakes-select", "size": 6}
            ),
        }
        labels = {
            "title": "笔记标题",
            "content": "笔记内容",
            "related_mistakes": "关联错题",
        }
        help_texts = {
            "related_mistakes": "选中需要引用的错题，我们会在笔记末尾展示它们。",
        }

    def __init__(self, *args, student=None, **kwargs):
        super().__init__(*args, **kwargs)
        self.student = student
        if student is not None:
            self.fields["related_mistakes"].queryset = student.mistakes.all()
        else:
            self.fields["related_mistakes"].queryset = Mistake.objects.none()

    def clean_content(self):
        content = self.cleaned_data["content"].strip()
        if not content:
            raise forms.ValidationError("笔记内容不能为空。")
        sanitized = sanitize_note_content(content)
        if not sanitized.strip():
            raise forms.ValidationError("笔记内容不能为空。")
        return sanitized

    def clean_related_mistakes(self):
        mistakes = self.cleaned_data["related_mistakes"]
        if self.student is None:
            return mistakes
        selected_ids = list(mistakes.values_list("pk", flat=True))
        valid_ids = set(
            self.student.mistakes.filter(pk__in=selected_ids).values_list(
                "pk", flat=True
            )
        )
        if set(selected_ids) != valid_ids:
            raise forms.ValidationError("只能关联自己的错题。")
        return mistakes
